#!/usr/bin/python

"""
File:       dm.py

Class:      CMPS 140 Winter 2011
Project:    Papa Bot
Date:       02/04/11
Authors:    Jason Erickson
            Morgan Davis
            DJ Visbal
            Constantine Perepelitsa
            Jae Kim
            Huu Huynh
"""

import sys
from sys import stderr
from user import User
from copy import deepcopy
from random import random, choice
import pickle
import operator

class DM(object):
    """Dialog Manager

    The Dialog Manager is the center of pappa bot. It is the first thing
    called, and maintains the state in the conversation with the user.

    The Dialog Manager tracks its state with a variable (state) that points to
    a function. When first initialized, state points to intro_state. At the
    conclusion of each state function, the next state function is returned.
    If None is returned instead of another state, the Dialog Manger quits.

    Testing:
        python dialog_manager.py
        Ask how to make a drink, include the word "make" or "try" and
        a drink name.
        eg:
        I want to try a White Russian
        How do you make a black monday
        I want a drink with scotch
        I want a drink with something...
    Attributes:
        nlu: Natural Language Understanding (input)
        nlg: Natural Language Generator (output)
        frame: filled by the nlu with the users intent
        action_lookup: maps action tag => state function
        state: points to the next state function
    """
    def __init__(self,debug_mode=False,nlu=None,db=None,nlg=None,user=None):
        self.debug_mode = debug_mode
        self.db = db
        self.nlu = nlu
        self.nlg = nlg
        self.user = user
        self.response = None

    def start_state_loop(self):
        self.state = self.intro_state
        # loop until we're ready to quit
        while self.state:
            self.state = self.state()

    def intro_state(self):
        """Find out what the user wants to do."""
        if self.debug_mode: print >>stderr, "intro_state"
        self.nlg.clear_screen()
        self.nlg.welcome_to_pappa()
        # make sure the user is logged in
        if self.user == None:
            self.user_login()
        # With a probibility of .5, ask the user if they want
        # a drink they had previously liked
        if random() >= .5:
            # get list of drinks the user likes
            liked = self.db.getUserLikedDrinks(self.user.id)
            if liked: # user has liked drinks?
                drink_id = choice(liked) # choose a random liked drink
                drink = self.db.getDrinkName(drink_id)
                self.nlg.ask_want_preferred_drink(self.user, drink)
                self.response = self.nlu.parse_user_response()
                if self.response.answer == 'y':
                    self.response.drink = drink
                    return self.get_recipe
                else:
                    self.nlg.affirmative() # acknowledge the user's decision
                return self.anything_else
        self.nlg.ask_for_task(self.user)
        self.response = self.nlu.parse_user_response()
        if self.response.action:
            return self.action_lookup(self.response.action)
        return self.confused

    def get_recipe(self):
        """Use the NLG to pretty print a recipe."""
        if self.debug_mode: print >>stderr, "get_recipe"
        if not self.response.drink: # didn't get drink name?
            # get a drink name or drink category
            self.nlg.ask_for_drink()
            self.response = self.nlu.parse_user_response()
            if not self.response.drink: # still no drink name
                if self.response.action: # user wants to do something else?
                    return self.action_lookup(self.response.action)
                else:
                    return self.confused
        drink_id = self.db.getDrinkID(self.response.drink)
        ingredients = self.db.getIngredientAmount(drink_id)
        instructions = self.db.getInstructions(drink_id)
        self.nlg.print_recipe(self.response.drink, ingredients, instructions)
        # if the user hasn't rated the drink, ask them to
        rated = self.db.getUserPreferences(self.user.id).keys()
        if drink_id not in rated:
            return self.get_user_preference
        return self.anything_else

    def list_drinks(self):
        """List drinks with ingredients specified by the user"""
        if self.debug_mode: print >>stderr, "list_drinks"
        if not self.response.ingredients: # didn't get any ingredients?
            self.nlg.ask_for_ingredient(self.user)
            self.response = self.nlu.parse_user_response()
            if not self.response.ingredients: # still no ingredients?
                return self.confused
        # print drink list
        self.nlg.print_drink_names_containing(self.response.ingredients,self.user)
        # ask user to pick one
        self.response = self.nlu.parse_user_response()
        if self.response.drink: # did we get a drink name?
            return self.get_recipe
        elif self.response.action: # user wants to do something else?
            return self.action_lookup(self.response.action)
        return self.confused

    def list_drinks_by_properties(self):
        """List drinks with user specified properties."""
        if self.debug_mode: print >>stderr, "list_drinks_with_preference"
        all_drink_ids = self.db.getAllDrinkIDs() # this is lazy... but simple
        pop_ids = all_drink_ids
        strong_ids = all_drink_ids
        if "POPULAR" in self.response.drink_properties:
            pop_ids = self.db.getPopularDrinkIDs(self.user.id)
        if "STRONG" in self.response.drink_properties:

            # compute the total amount of alcohol in each drink in ounces (yes, pure alcohol)
            # to see how these pickles were created, see utilities/pickle db.py

            # load ingredientID:alc% mapping
            ingredients_alcohol = pickle.load(open("ingredient_alcohol.pickle","r"))
            # load drinkID:ingredientPosition:(ingredientID,ounces) mapping
            drink_ingr_ounces = pickle.load(open("drink_ingredient_ounces.pickle","r"))
            
            drink_alc = []
            all_drinks = all_drink_ids
            for d in all_drinks:
                alc_ounces = 0
                total_ounces = 0
                ingr_ounces = drink_ingr_ounces[d]
                for i in ingr_ounces.values():
                    alc_perc = ingredients_alcohol[i[0]]
                    ounces = i[1]
                    total_ounces += ounces
                    alc_ounces += ounces*alc_perc
                ratio = 0
                if total_ounces != 0:
                    ratio = alc_ounces/total_ounces
                if ratio >= .3 and ratio < .7:
                    drink_alc.append((d,alc_ounces))
            
            # sort by alcohol content
            strong_ids = [d for (d,a) in sorted(drink_alc,key=lambda d: d[1],reverse=True)]

        # disliked drinks are removed in the NLG in print_drink_names()
        # since strong drinks are sorted, we can't use sets...
        intersect_ids = []
        for s in strong_ids: # important to look on strong_ids to preserve ordering
            if s in pop_ids:
                intersect_ids.append(s)
            
        # print drink list
        self.nlg.print_drinks_for_properties(intersect_ids,self.response.drink_properties,self.user)
        # ask user to pick one
        self.response = self.nlu.parse_user_response()
        if self.response.drink: # did we get a drink name?
            return self.get_recipe
            
        return self.confused

    def list_drinks_by_category(self):
        """List drinks from a category."""
        if self.debug_mode: print >>stderr, "list_drinks_by_category"
        cat_id = self.db.getCategoryID(self.response.category)
        drink_ids = self.db.getDrinkIDsInCategory(cat_id)
        self.nlg.ask_for_drink_in_category(self.response.category, drink_ids, self.user)
        self.response = self.nlu.parse_user_response()
        if self.response.drink: # did we get a drink name?
            return self.get_recipe
        elif self.response.action: # user wants to do something else?
            return self.action_lookup(self.response.action)
        return self.confused

    def list_drinks_from_cabinet(self):
        """List drinks that only use ingredients from the user's cabinet."""
        if self.debug_mode: print >>stderr, "list_drinks_from_cabinet"
        # print drink list
        canMake = self.nlg.print_drink_names_withonly(self.user)
        if canMake:
            # ask user to pick one
            self.response = self.nlu.parse_user_response()
            if self.response.drink: # did we get a drink name?
                return self.get_recipe
            elif self.response.action: # user wants to do something else?
                return self.action_lookup(self.response.action)
        else:
            return self.anything_else

    def recommend_drinks(self):
        """Give the user a drink recommendation."""
        # I'm real happy for you, Morgan, and I'mma let you finish, but
        # I just wrote the best recommend_drinks method of all time.
        # OF ALL TIME.
        liked = self.db.getUserLikedDrinks(self.user.id)
        if not liked:
            self.nlg.print_recommended_drinks(None)
        else:
            self.nlg.print_recommended_drinks(self.user.id)
        self.response = self.nlu.parse_user_response()
        if self.response.drink: # did we get a drink name?
            return self.get_recipe
        elif self.response.action: # user wants to do something else?
            return self.action_lookup(self.response.action)
        return self.confused
       #return self.anything_else

    def list_cabinet(self):
        """List the ingredients in the users cabinet."""
        if self.debug_mode: print >>stderr, "list_cabinet"
        self.nlg.show_cabinet(self.user.get_cabinet(),False)
        return self.anything_else

    def cabinet_add(self):
        """Add ingredient(s) to the user's cabinet."""
        if self.debug_mode: print >>stderr, "cabinet_add"
        if not self.response.ingredients: # didn't get any ingredients?
            self.nlg.ask_for_ingredient(self.user)
            self.response = self.nlu.parse_user_response()
            if not self.response.ingredients: # still no ingredients?
                if self.response.action: # user wants to do something else?
                    return self.action_lookup(self.response.action)
                else:
                    return self.confused
        modified = False
        added = []
        not_added = [] # duplicates
        ingredient_ids = [self.db.getIngredientID(i) \
                         for i in self.response.ingredients]
        cab_ingredients = self.user.get_cabinet()
        for id in ingredient_ids:
            if id in cab_ingredients: # duplicate
                not_added.append(id)
            else:
                self.user.cabinet_add(id)
                added.append(id)
        if not_added: # any duplicates?
            self.nlg.duplicate_ingredients(not_added)
        if added: # anything left after removing duplicates?
            self.nlg.adding_to_cabinet(added)
            modified = True
        self.nlg.show_cabinet(self.user.get_cabinet(), modified)
        return self.anything_else

    def cabinet_remove(self):
        """Remove ingredient(s) from the user's cabinet."""
        if self.debug_mode: print >>stderr, "cabinet_remove"
        if not self.response.ingredients: # didn't get any ingredients?
            self.nlg.ask_for_ingredient(self.user)
            self.response = self.nlu.parse_user_response()
            if not self.response.ingredients: # still no ingredients?
                if self.response.action: # user wants to do something else?
                    return self.action_lookup(self.response.action)
                else:
                    return self.confused
        modified = False
        removed = []
        not_removed = [] # not in cabinet
        ingredient_ids = [self.db.getIngredientID(i) \
                         for i in self.response.ingredients]
        cabinet = self.user.get_cabinet()
        for id in ingredient_ids:
            if id in cabinet:
                self.user.cabinet_remove(id)
                removed.append(id)
            else: # not in cabinet
                not_removed.append(id)
        if not_removed: # anything not in cabinet?
            self.nlg.not_in_cabinet(not_removed)
        if removed: # anything removed?
            self.nlg.removing_from_cabinet(removed)
            modified = True
        self.nlg.show_cabinet(self.user.get_cabinet(), modified)
        return self.anything_else
    
    def kill_me(self):
        """The user wants a drink that will kill them."""
        # this was added at the last minute just for fun.
        
        # load ingredientID:alc% mapping
        ingredients_alcohol = pickle.load(open("ingredient_alcohol.pickle","r"))
        # load drinkID:ingredientPosition:(ingredientID,ounces) mapping
        drink_ingr_ounces = pickle.load(open("drink_ingredient_ounces.pickle","r"))
        
        drink_alc = []
        all_drinks = self.db.getAllDrinkIDs()
        for d in all_drinks:
            alc_ounces = 0
            total_ounces = 0
            ingr_ounces = drink_ingr_ounces[d]
            for i in ingr_ounces.values():
                alc_perc = ingredients_alcohol[i[0]]
                ounces = i[1]
                total_ounces += ounces
                alc_ounces += ounces*alc_perc
            #drink_alc.append((d,alc_ounces))
            ratio = 0
            if total_ounces != 0:
                ratio = alc_ounces/total_ounces
            if ratio >= .39 and ratio < .8 and total_ounces >= 15:
                drink_alc.append((d,alc_ounces))
        
        # sort by alcohol content
        strong_ids = [d for (d,a) in sorted(drink_alc,key=lambda d: d[1],reverse=True)]
        
        # get a random drink from the top 10
        drink_id = choice(strong_ids[:10])

        # print the drink, and quit!
        self.nlg.kbye(drink_id)
        return None

    def get_user_preference(self):
        """Get a rating from the user for the drink they just tried"""
        if self.debug_mode: print >>stderr, "get_user_preference"
        drink = self.response.drink
        self.nlg.ask_user_preference(self.response.drink)
        self.response = self.nlu.parse_user_response()
        drink_id = self.db.getDrinkID(drink)
        if self.response.answer == "y":
            self.db.addToUserPreferences(self.user.id, drink_id, 1)
            self.nlg.glad()
        else:
            self.db.addToUserPreferences(self.user.id, drink_id, 0)
            self.nlg.sorry()
        return self.anything_else

    def anything_else(self):
        """Check if the user wants to do anything else"""
        if self.debug_mode: print >>stderr, "anything_else"
        self.nlg.ask_anything_else(self.user)
        self.response = self.nlu.parse_user_response()
        # 3 possible cases for user response
        if self.response.action: # case 1: user asks another question
            return self.action_lookup(self.response.action)
        elif self.response.answer == "y": # case 2: user just says yes
            return self.intro_state
        elif self.response.answer == "n": # case 3: user says no
            self.nlg.goodbye()
            return None  # ready to quit
        # y/n not specified, and no question asked
        return self.confused

    def confused(self):
        """Pappa-Bot didn't understand the users last response. Let's try to
           clarify what the user meant."""
        if self.debug_mode: print >>stderr, "confused"
        self.nlg.confused()
        self.response = self.nlu.parse_user_response()
        if self.response.action: # user asked another question
            return self.action_lookup(self.response.action)
        # we don't know wtf the user wants; say we're confused and quit
        self.nlg.wtf()
        return None # ready to quit

    def quit(self):
        """Send the user a goodbye message, and quit."""
        if self.debug_mode: print >>stderr, "quit"
        self.nlg.goodbye()
        return None # ready to quit

    def user_login(self):
        """Get the user's name."""
        if self.debug_mode: print >>stderr, "user_login"
        self.nlg.ask_username()
        uname = self.nlu.get_name()
        self.user = User(self.db,uname)

    def action_lookup(self, action):
        return getattr(self, action)


if __name__ == "__main__":
    print "Use \"python papa-bot.py\" instead."
   #dm = DialogManager()

